feat: add DataProvider and OperationProvider plugin interfaces#355
feat: add DataProvider and OperationProvider plugin interfaces#355
Conversation
New provider interfaces for plugins that serve standard SOVD data and operation resources on plugin-created entities. Per-entity routing (unlike singleton Log/Script/Update providers) allows multiple plugins to each handle their own entity sets.
PluginLoader queries get_data_provider() and get_operation_provider() C exports from plugin .so files, following the same pattern as existing get_log_provider/get_script_provider queries.
Entity ownership map (entity_id -> plugin_name) enables per-entity routing to DataProvider/OperationProvider. Populated from IntrospectionProvider results.
Plugin entities now appear in the entity cache regardless of discovery mode (runtime_only, manifest_only, hybrid). In hybrid mode, plugins participate in the merge pipeline as before. In other modes, entities are injected during the regular cache refresh cycle. All plugin entities get source='plugin'. Entity ownership is tracked in PluginManager for per-entity provider routing.
EntityInfo now has is_plugin and plugin_name fields, populated during entity lookup from PluginManager's ownership map. Enables handlers to check entity ownership without querying PluginManager directly.
Data handlers (list, read, write) check EntityInfo.is_plugin and delegate to the owning plugin's DataProvider instead of querying ROS 2 topics via DataAccessManager.
Operation handlers (list, execute) check EntityInfo.is_plugin and delegate to the owning plugin's OperationProvider instead of looking up ROS 2 services/actions via OperationManager.
When a plugin entity has DataProvider or OperationProvider registered, the entity detail response automatically includes 'data' and 'operations' in the capabilities array.
Tests verify entity ownership tracking in PluginManager: registration, lookup, multi-plugin coexistence, provider resolution via add_plugin (in-process dynamic_cast), and nullptr returns for unknown entities and plugins without providers.
daf6a84 to
81db09c
Compare
… ops The move constructor and move assignment operator were missing data_provider and operation_provider fields, causing them to be silently lost during plugin loading. This made get_data_provider_for_entity() always return nullptr even though dlsym successfully discovered the providers.
New provider interface enabling plugins to serve SOVD fault endpoints
(GET /faults, GET /faults/{code}, DELETE /faults/{code}) on their
entities. Follows the same per-entity routing pattern as DataProvider
and OperationProvider.
Fault handlers delegate to FaultProvider for plugin-owned entities.
Capabilities auto-deduction includes 'faults' when FaultProvider is
registered.
There was a problem hiding this comment.
Pull request overview
This PR extends the gateway plugin framework to support per-entity delegation of standard SOVD resource collections by introducing new provider interfaces (Data/Operation, plus Fault in this diff) and routing requests to the owning plugin for plugin-created entities.
Changes:
- Add
DataProvider,OperationProvider, andFaultProviderinterfaces and load them viadlsyminPluginLoader. - Track entity ownership in
PluginManagerand tag entities for handler routing (EntityInfo::is_plugin/plugin_name). - Delegate
/data,/operations, and/faultshandler logic to plugin providers for plugin-owned entities; auto-add capabilities when providers are present.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/providers/data_provider.hpp | New per-entity DataProvider plugin API. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/providers/operation_provider.hpp | New per-entity OperationProvider plugin API. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/providers/fault_provider.hpp | New per-entity FaultProvider plugin API. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_loader.hpp | Extend load result to include new provider pointers and export symbols. |
| src/ros2_medkit_gateway/src/plugins/plugin_loader.cpp | Discover new providers via dlsym(get_*_provider) and store pointers. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp | Add entity ownership map + provider resolution APIs. |
| src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp | Implement ownership registration + per-entity provider lookup. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/handler_context.hpp | Add plugin routing fields to EntityInfo. |
| src/ros2_medkit_gateway/src/http/handlers/handler_context.cpp | Populate EntityInfo::is_plugin/plugin_name from PluginManager ownership. |
| src/ros2_medkit_gateway/src/http/handlers/data_handlers.cpp | Delegate /data endpoints to plugin DataProvider for plugin-owned entities. |
| src/ros2_medkit_gateway/src/http/handlers/operation_handlers.cpp | Delegate /operations endpoints to plugin OperationProvider for plugin-owned entities. |
| src/ros2_medkit_gateway/src/http/handlers/fault_handlers.cpp | Delegate /faults endpoints to plugin FaultProvider for plugin-owned entities. |
| src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp | Auto-add data/operations/faults capabilities when providers resolve for an entity. |
| src/ros2_medkit_gateway/src/discovery/layers/plugin_layer.cpp | Tag plugin-created entities with source="plugin" in hybrid pipeline. |
| src/ros2_medkit_gateway/src/gateway_node.cpp | Register introspection providers in more modes; inject plugin entities in non-hybrid refresh. |
| src/ros2_medkit_gateway/test/test_plugin_entity_routing.cpp | New unit tests for ownership tracking and provider resolution. |
| src/ros2_medkit_gateway/CMakeLists.txt | Add new gtest target for plugin entity routing. |
Comments suppressed due to low confidence (2)
src/ros2_medkit_gateway/src/http/handlers/operation_handlers.cpp:395
- For plugin-owned entities, POST /operations/{id} bypasses lock enforcement (validate_lock_access is only applied on the ROS2 path). Apply validate_lock_access() before calling OperationProvider::execute_operation() so plugin-backed operations honor the same locking semantics.
// Delegate to plugin OperationProvider if entity is plugin-owned
if (entity_info.is_plugin) {
auto * pmgr = ctx_.node()->get_plugin_manager();
auto * op_prov = pmgr ? pmgr->get_operation_provider_for_entity(entity_id) : nullptr;
if (op_prov) {
json params = json::object();
if (!req.body.empty()) {
params = json::parse(req.body, nullptr, false);
if (params.is_discarded()) {
HandlerContext::send_error(res, 400, ERR_INVALID_REQUEST, "Invalid JSON body");
return;
}
}
auto result = op_prov->execute_operation(entity_id, operation_id, params);
if (result) {
HandlerContext::send_json(res, *result);
} else {
HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message);
}
return;
}
HandlerContext::send_error(res, 404, ERR_OPERATION_NOT_FOUND,
"No operation provider for plugin entity '" + entity_id + "'");
return;
}
// Check lock access for operations
if (ctx_.validate_lock_access(req, res, entity_info, "operations")) {
return;
}
src/ros2_medkit_gateway/src/http/handlers/fault_handlers.cpp:676
- For plugin-owned entities, DELETE /faults/{code} bypasses lock enforcement (validate_lock_access is only applied on the ROS2 path). Apply validate_lock_access() before calling FaultProvider::clear_fault() so plugin-backed fault clearing honors the same locking semantics.
// Delegate to plugin FaultProvider if entity is plugin-owned
if (entity_info.is_plugin) {
auto * pmgr = ctx_.node()->get_plugin_manager();
auto * fault_prov = pmgr ? pmgr->get_fault_provider_for_entity(entity_id) : nullptr;
if (fault_prov) {
auto result = fault_prov->clear_fault(entity_id, fault_code);
if (result) {
HandlerContext::send_json(res, *result);
} else {
HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message);
}
return;
}
HandlerContext::send_error(res, 404, ERR_RESOURCE_NOT_FOUND,
"No fault provider for plugin entity '" + entity_id + "'");
return;
}
// Check lock access for faults
if (ctx_.validate_lock_access(req, res, entity_info, "faults")) {
return;
}
| if (result) { | ||
| HandlerContext::send_json(res, *result); | ||
| } else { | ||
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); |
There was a problem hiding this comment.
The plugin delegation path sends errors using the literal "x-plugin-error". This is not a valid SOVD standard error code and it also won’t be treated as a vendor error by HandlerContext::send_error() (only x-medkit-* triggers vendor_error + vendor_code). Please use a defined error code from error_codes.hpp (e.g., ERR_VENDOR_ERROR with a vendor_code like x-medkit-plugin-error, or introduce a dedicated ERR_X_MEDKIT_PLUGIN_ERROR constant) and include useful parameters (entity_id, plugin_name) where available.
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); | |
| HandlerContext::send_error( | |
| res, result.error().http_status, ERR_VENDOR_ERROR, result.error().message, | |
| json{{"vendor_code", "x-medkit-plugin-error"}, {"entity_id", entity_id}}); |
| // Delegate to plugin DataProvider if entity is plugin-owned | ||
| if (entity_opt->is_plugin) { | ||
| auto * pmgr = ctx_.node()->get_plugin_manager(); | ||
| auto * data_prov = pmgr ? pmgr->get_data_provider_for_entity(entity_id) : nullptr; | ||
| if (data_prov) { | ||
| json value; | ||
| if (!req.body.empty()) { | ||
| value = json::parse(req.body, nullptr, false); | ||
| if (value.is_discarded()) { | ||
| HandlerContext::send_error(res, 400, ERR_INVALID_REQUEST, "Invalid JSON body"); | ||
| return; | ||
| } | ||
| } | ||
| auto result = data_prov->write_data(entity_id, topic_name, value); | ||
| if (result) { | ||
| HandlerContext::send_json(res, *result); | ||
| } else { | ||
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); | ||
| } | ||
| return; | ||
| } | ||
| HandlerContext::send_error(res, 404, ERR_RESOURCE_NOT_FOUND, | ||
| "No data provider for plugin entity '" + entity_id + "'"); | ||
| return; | ||
| } | ||
|
|
||
| // Check lock access for data | ||
| if (ctx_.validate_lock_access(req, res, *entity_opt, "data")) { | ||
| return; |
There was a problem hiding this comment.
For plugin-owned entities, PUT /data bypasses lock enforcement (validate_lock_access is only applied on the ROS2 path). This allows modifying a locked collection without holding a lock when a plugin provider is used. Apply the same validate_lock_access() check before calling DataProvider::write_data().
| // Delegate to plugin DataProvider if entity is plugin-owned | ||
| if (entity_info.is_plugin) { | ||
| auto * pmgr = ctx_.node()->get_plugin_manager(); | ||
| auto * data_prov = pmgr ? pmgr->get_data_provider_for_entity(entity_id) : nullptr; | ||
| if (data_prov) { | ||
| auto result = data_prov->list_data(entity_id); | ||
| if (result) { | ||
| HandlerContext::send_json(res, *result); | ||
| } else { | ||
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); | ||
| } | ||
| return; | ||
| } | ||
| HandlerContext::send_json(res, json{{"items", json::array()}}); | ||
| return; | ||
| } |
There was a problem hiding this comment.
New plugin delegation behavior in /data handlers isn’t covered by the existing DataHandlers gtests. Add unit tests (in test_data_handlers.cpp) that exercise plugin-owned entities for list/read/write, including error mapping and lock enforcement behavior.
| // Delegate to plugin OperationProvider if entity is plugin-owned | ||
| if (entity_info.is_plugin) { | ||
| auto * pmgr = ctx_.node()->get_plugin_manager(); | ||
| auto * op_prov = pmgr ? pmgr->get_operation_provider_for_entity(entity_id) : nullptr; | ||
| if (op_prov) { | ||
| auto result = op_prov->list_operations(entity_id); | ||
| if (result) { | ||
| HandlerContext::send_json(res, *result); | ||
| } else { | ||
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); | ||
| } | ||
| return; | ||
| } | ||
| HandlerContext::send_json(res, json{{"items", json::array()}}); | ||
| return; |
There was a problem hiding this comment.
The plugin delegation path sends errors using the literal "x-plugin-error" which is not a valid SOVD error_code and won’t be treated as a vendor error. Use a constant from error_codes.hpp and a vendor code with the x-medkit-* prefix (or add a dedicated ERR_X_MEDKIT_PLUGIN_ERROR constant) so responses follow the GenericError schema.
| void OperationHandlers::handle_create_execution(const httplib::Request & req, httplib::Response & res) { | ||
| std::string entity_id; | ||
| std::string operation_id; | ||
| try { | ||
| if (req.matches.size() < 3) { | ||
| HandlerContext::send_error(res, 400, ERR_INVALID_REQUEST, "Invalid request"); | ||
| return; | ||
| } | ||
|
|
||
| entity_id = req.matches[1]; | ||
| operation_id = req.matches[2]; | ||
|
|
||
| // Validate entity ID and type for this route | ||
| auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id); | ||
| if (!entity_opt) { | ||
| return; // Response already sent (error or forwarded to peer) | ||
| } | ||
| auto entity_info = *entity_opt; | ||
|
|
||
| // Delegate to plugin OperationProvider if entity is plugin-owned | ||
| if (entity_info.is_plugin) { | ||
| auto * pmgr = ctx_.node()->get_plugin_manager(); | ||
| auto * op_prov = pmgr ? pmgr->get_operation_provider_for_entity(entity_id) : nullptr; | ||
| if (op_prov) { | ||
| json params = json::object(); | ||
| if (!req.body.empty()) { | ||
| params = json::parse(req.body, nullptr, false); | ||
| if (params.is_discarded()) { | ||
| HandlerContext::send_error(res, 400, ERR_INVALID_REQUEST, "Invalid JSON body"); | ||
| return; | ||
| } | ||
| } | ||
| auto result = op_prov->execute_operation(entity_id, operation_id, params); | ||
| if (result) { | ||
| HandlerContext::send_json(res, *result); | ||
| } else { | ||
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); | ||
| } | ||
| return; | ||
| } | ||
| HandlerContext::send_error(res, 404, ERR_OPERATION_NOT_FOUND, | ||
| "No operation provider for plugin entity '" + entity_id + "'"); | ||
| return; | ||
| } |
There was a problem hiding this comment.
New plugin delegation behavior in /operations handlers isn’t covered by the existing OperationHandlers tests. Add unit tests that cover plugin-owned entities for list and execute, including JSON body parsing failures, error mapping, and lock enforcement.
| // Register entity ownership for per-entity provider routing | ||
| for (auto & [name, provider] : introspection_providers) { | ||
| IntrospectionInput input; | ||
| auto result = provider->introspect(input); | ||
| std::vector<std::string> entity_ids; | ||
| for (const auto & area : result.new_entities.areas) { | ||
| entity_ids.push_back(area.id); | ||
| } | ||
| for (const auto & comp : result.new_entities.components) { | ||
| entity_ids.push_back(comp.id); | ||
| } | ||
| for (const auto & app : result.new_entities.apps) { | ||
| entity_ids.push_back(app.id); | ||
| } | ||
| for (const auto & func : result.new_entities.functions) { | ||
| entity_ids.push_back(func.id); | ||
| } | ||
| plugin_mgr_->register_entity_ownership(name, entity_ids); | ||
| } |
There was a problem hiding this comment.
Entity ownership is registered by calling IntrospectionProvider::introspect() with an empty IntrospectionInput during node construction. IntrospectionProvider is defined to depend on the current discovery snapshot; using an empty input can produce a different set of entities than the actual discovery cycle (especially in HYBRID mode), causing handlers to miss plugin routing. Ownership should be registered from the same introspection results used during discovery/refresh (with the real IntrospectionInput) rather than an extra empty-input call here.
| // Register entity ownership for per-entity provider routing | |
| for (auto & [name, provider] : introspection_providers) { | |
| IntrospectionInput input; | |
| auto result = provider->introspect(input); | |
| std::vector<std::string> entity_ids; | |
| for (const auto & area : result.new_entities.areas) { | |
| entity_ids.push_back(area.id); | |
| } | |
| for (const auto & comp : result.new_entities.components) { | |
| entity_ids.push_back(comp.id); | |
| } | |
| for (const auto & app : result.new_entities.apps) { | |
| entity_ids.push_back(app.id); | |
| } | |
| for (const auto & func : result.new_entities.functions) { | |
| entity_ids.push_back(func.id); | |
| } | |
| plugin_mgr_->register_entity_ownership(name, entity_ids); | |
| } | |
| // Entity ownership must be registered from the same introspection results used by the | |
| // discovery refresh cycle. Do not call provider->introspect() here with a default/empty | |
| // IntrospectionInput, as that can diverge from the actual discovery snapshot and lead to | |
| // incorrect per-entity plugin routing. |
| // Inject plugin entities for non-hybrid modes. | ||
| // In hybrid mode, plugin entities are merged via the pipeline (PluginLayer). | ||
| // In runtime_only/manifest_only modes, we append them directly. | ||
| if (discovery_mgr_->get_mode() != DiscoveryMode::HYBRID && plugin_mgr_ && plugin_mgr_->has_plugins()) { | ||
| auto providers = plugin_mgr_->get_named_introspection_providers(); | ||
| for (auto & [name, provider] : providers) { | ||
| IntrospectionInput input; | ||
| auto result = provider->introspect(input); | ||
| for (auto & area : result.new_entities.areas) { | ||
| area.source = "plugin"; | ||
| areas.push_back(std::move(area)); | ||
| } | ||
| for (auto & comp : result.new_entities.components) { | ||
| comp.source = "plugin"; | ||
| all_components.push_back(std::move(comp)); | ||
| } | ||
| for (auto & app : result.new_entities.apps) { | ||
| app.source = "plugin"; | ||
| apps.push_back(std::move(app)); | ||
| } | ||
| for (auto & func : result.new_entities.functions) { | ||
| func.source = "plugin"; | ||
| functions.push_back(std::move(func)); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
In refresh_cache(), plugin entities are injected for non-hybrid modes but entity ownership routing is not updated here. This can leave PluginManager::entity_ownership_ stale (or missing new entities) and cause plugin entities to be treated as non-plugin in handlers. Consider building IntrospectionInput from the current discovered entities (areas/components/apps/functions), calling introspect(), injecting new_entities, and updating/replacing ownership mapping in the same loop (ideally clearing old ownership for that plugin each refresh).
| void PluginManager::register_entity_ownership(const std::string & plugin_name, | ||
| const std::vector<std::string> & entity_ids) { | ||
| std::unique_lock<std::shared_mutex> lock(plugins_mutex_); | ||
| for (const auto & eid : entity_ids) { | ||
| entity_ownership_[eid] = plugin_name; | ||
| } | ||
| } |
There was a problem hiding this comment.
register_entity_ownership() only inserts/overwrites entries but never removes stale ownership mappings (e.g., entities that disappear on a later refresh, or IDs later reused by non-plugin entities). This can cause incorrect is_plugin tagging and handler delegation. Consider making ownership registration replace the previous set for the plugin (or rebuild/clear entity_ownership_ on each cache refresh) and clear mappings when a plugin is disabled/unloaded.
| ei.is_plugin = true; | ||
| ei.plugin_name = *owner; |
There was a problem hiding this comment.
HandlerContext marks entities as plugin-owned purely from PluginManager::get_entity_owner(), without checking that the owning plugin is currently loaded or that any provider exists for the requested collection. If entity_ownership_ becomes stale, this can force handlers down the plugin path and change responses unexpectedly. Consider verifying the plugin is still active (or that at least one provider resolves for the entity) before setting is_plugin=true.
| ei.is_plugin = true; | |
| ei.plugin_name = *owner; | |
| // Only treat the entity as plugin-backed if the owning plugin is still | |
| // active enough to resolve a provider. This avoids stale ownership | |
| // metadata forcing handlers down the plugin path unexpectedly. | |
| auto introspection_provider = pmgr->get_introspection_provider(*owner); | |
| if (introspection_provider) { | |
| ei.is_plugin = true; | |
| ei.plugin_name = *owner; | |
| } |
| /** | ||
| * @brief Provider interface for entity data resources | ||
| * | ||
| * Typed provider interface for plugins that serve SOVD data resources | ||
| * (GET /{entity_type}/{id}/data, GET /{entity_type}/{id}/data/{name}). | ||
| * Unlike LogProvider/ScriptProvider (singletons), multiple DataProvider | ||
| * plugins can coexist - each handles its own set of entities. | ||
| * | ||
| * Entity ownership is determined by IntrospectionProvider: entities | ||
| * created by a plugin's introspect() are routed to that plugin's | ||
| * DataProvider. | ||
| * | ||
| * @par Thread safety | ||
| * All methods may be called from multiple HTTP handler threads concurrently. | ||
| * Implementations must provide their own synchronization. | ||
| * | ||
| * @see GatewayPlugin for the base class all plugins must also implement | ||
| * @see OperationProvider for the operations counterpart | ||
| */ |
There was a problem hiding this comment.
These new provider interfaces are part of the public plugin API (including the required dlsym export names in plugin_loader). The PR currently doesn’t update any developer-facing docs describing how to implement/export DataProvider/OperationProvider (and expected JSON schemas / error mapping). Please add documentation in the appropriate docs section (e.g., docs/design or a plugin author guide) and reference it here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (3)
src/ros2_medkit_gateway/src/http/handlers/data_handlers.cpp:277
- For plugin-owned entities,
PUT /data/...bypassesvalidate_lock_access()and calls the provider directly. This allows mutating requests without the lock/X-Client-Idenforcement used for non-plugin entities; the lock check should be applied before delegating to the plugin provider as well.
// Delegate to plugin DataProvider if entity is plugin-owned
if (entity_opt->is_plugin) {
auto * pmgr = ctx_.node()->get_plugin_manager();
auto * data_prov = pmgr ? pmgr->get_data_provider_for_entity(entity_id) : nullptr;
if (data_prov) {
json value;
if (!req.body.empty()) {
value = json::parse(req.body, nullptr, false);
if (value.is_discarded()) {
HandlerContext::send_error(res, 400, ERR_INVALID_REQUEST, "Invalid JSON body");
return;
}
}
auto result = data_prov->write_data(entity_id, topic_name, value);
if (result) {
HandlerContext::send_json(res, *result);
} else {
HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message);
}
return;
}
HandlerContext::send_error(res, 404, ERR_RESOURCE_NOT_FOUND,
"No data provider for plugin entity '" + entity_id + "'");
return;
}
// Check lock access for data
if (ctx_.validate_lock_access(req, res, *entity_opt, "data")) {
return;
}
src/ros2_medkit_gateway/src/http/handlers/operation_handlers.cpp:395
- For plugin-owned entities, operation execution is delegated before calling
validate_lock_access(). This bypasses the lock manager and allows operations to run without required lock ownership; apply the same lock validation to plugin-owned entities before callingexecute_operation().
// Delegate to plugin OperationProvider if entity is plugin-owned
if (entity_info.is_plugin) {
auto * pmgr = ctx_.node()->get_plugin_manager();
auto * op_prov = pmgr ? pmgr->get_operation_provider_for_entity(entity_id) : nullptr;
if (op_prov) {
json params = json::object();
if (!req.body.empty()) {
params = json::parse(req.body, nullptr, false);
if (params.is_discarded()) {
HandlerContext::send_error(res, 400, ERR_INVALID_REQUEST, "Invalid JSON body");
return;
}
}
auto result = op_prov->execute_operation(entity_id, operation_id, params);
if (result) {
HandlerContext::send_json(res, *result);
} else {
HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message);
}
return;
}
HandlerContext::send_error(res, 404, ERR_OPERATION_NOT_FOUND,
"No operation provider for plugin entity '" + entity_id + "'");
return;
}
// Check lock access for operations
if (ctx_.validate_lock_access(req, res, entity_info, "operations")) {
return;
}
src/ros2_medkit_gateway/src/http/handlers/fault_handlers.cpp:676
- For plugin-owned entities,
clear_faultdelegates to the plugin provider beforevalidate_lock_access(), bypassing the lock manager used for non-plugin entities. Apply the same lock validation to plugin-owned entities before clearing faults.
// Delegate to plugin FaultProvider if entity is plugin-owned
if (entity_info.is_plugin) {
auto * pmgr = ctx_.node()->get_plugin_manager();
auto * fault_prov = pmgr ? pmgr->get_fault_provider_for_entity(entity_id) : nullptr;
if (fault_prov) {
auto result = fault_prov->clear_fault(entity_id, fault_code);
if (result) {
HandlerContext::send_json(res, *result);
} else {
HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message);
}
return;
}
HandlerContext::send_error(res, 404, ERR_RESOURCE_NOT_FOUND,
"No fault provider for plugin entity '" + entity_id + "'");
return;
}
// Check lock access for faults
if (ctx_.validate_lock_access(req, res, entity_info, "faults")) {
return;
}
| auto result = data_prov->list_data(entity_id); | ||
| if (result) { | ||
| HandlerContext::send_json(res, *result); | ||
| } else { | ||
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); | ||
| } | ||
| return; |
There was a problem hiding this comment.
The handler returns plugin provider failures using a string literal error code ("x-plugin-error"). HandlerContext::send_error() expects SOVD/medkit error-code constants (and vendor errors must use the x-medkit- prefix to populate vendor_code). Add a proper constant in error_codes.hpp (e.g., x-medkit-plugin-error) and use it here so clients get a schema-compliant error response.
| if (result) { | ||
| HandlerContext::send_json(res, *result); | ||
| } else { | ||
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); |
There was a problem hiding this comment.
Same as DataHandlers: plugin provider failures are returned using the literal error code "x-plugin-error". Use a defined error-code constant from error_codes.hpp (with x-medkit- prefix if vendor-specific) so send_error() produces a schema-compliant SOVD GenericError response.
| HandlerContext::send_error(res, result.error().http_status, "x-plugin-error", result.error().message); | |
| HandlerContext::send_error( | |
| res, result.error().http_status, ERR_SERVICE_UNAVAILABLE, result.error().message); |
| /// Non-owning pointer to DataProvider interface (null if not provided). | ||
| /// Unlike LogProvider/ScriptProvider, multiple plugins can each provide data for different entities. | ||
| DataProvider * data_provider = nullptr; | ||
|
|
||
| /// Non-owning pointer to OperationProvider interface (null if not provided). | ||
| OperationProvider * operation_provider = nullptr; | ||
|
|
||
| /// Non-owning pointer to FaultProvider interface (null if not provided). | ||
| FaultProvider * fault_provider = nullptr; | ||
|
|
There was a problem hiding this comment.
This PR adds a new FaultProvider interface and handler delegation (get_fault_provider_for_entity / get_fault_provider symbol), but the PR title/summary focus on DataProvider + OperationProvider. Either update the PR description/title to reflect FaultProvider being included, or split FaultProvider into a separate change to keep scope and review surface aligned.
| /// Append plugin-registered capabilities to a capabilities JSON array | ||
| void append_plugin_capabilities(json & capabilities, const std::string & entity_type_path, | ||
| const std::string & entity_id, SovdEntityType entity_type, const GatewayNode * node) { | ||
| auto * pmgr = node->get_plugin_manager(); | ||
| if (!pmgr || !pmgr->get_context()) { | ||
| if (!pmgr) { | ||
| return; | ||
| } | ||
| auto * ctx = pmgr->get_context(); | ||
|
|
||
| std::string href_prefix; | ||
| href_prefix.reserve(64); | ||
| href_prefix.append("/api/v1/").append(entity_type_path).append("/").append(entity_id).append("/"); | ||
|
|
||
| // Auto-add standard capabilities based on registered providers | ||
| if (pmgr->get_data_provider_for_entity(entity_id) && !has_capability(capabilities, "data")) { | ||
| capabilities.push_back({{"name", "data"}, {"href", href_prefix + "data"}}); | ||
| } | ||
| if (pmgr->get_operation_provider_for_entity(entity_id) && !has_capability(capabilities, "operations")) { | ||
| capabilities.push_back({{"name", "operations"}, {"href", href_prefix + "operations"}}); | ||
| } | ||
| if (pmgr->get_fault_provider_for_entity(entity_id) && !has_capability(capabilities, "faults")) { | ||
| capabilities.push_back({{"name", "faults"}, {"href", href_prefix + "faults"}}); | ||
| } | ||
|
|
There was a problem hiding this comment.
Capability auto-deduction now depends on per-entity plugin provider routing. Please update the REST API docs (docs/api/rest.rst) to describe how plugin-owned entities expose standard collections (/data, /operations, /faults) and how provider failures are surfaced to clients, since this changes discovery/capabilities behavior.
Pull Request
Summary
Add per-entity provider routing for gateway plugins, enabling plugins to serve standard SOVD data and operation endpoints on entities they create via IntrospectionProvider.
DataProviderandOperationProviderinterfaces (per-entity routing, unlike singleton Log/Script/Update providers)get_data_provider,get_operation_provider)source = "plugin"Issue
Type
Testing
Checklist